{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f7554354",
   "metadata": {},
   "source": [
    "# RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval\n",
    "\n",
    "[RAPTOR](https://arxiv.org/pdf/2401.18059.pdf) 논문은 문서의 색인 생성 및 검색에 대한 흥미로운 접근 방식을 제시합니다.\n",
    "\n",
    "[테디노트 논문 요약글(노션)](https://teddylee777.notion.site/RAPTOR-e835d306fc664dc2ad76191dee1cd859?pvs=4)\n",
    "\n",
    "- `leafs` 는 가장 low-level 의 시작 문서 집합입니다. 이 문서들은 임베딩되어 클러스터링됩니다.\n",
    "- 그런 다음 클러스터는 유사한 문서들 간의 정보를 더 높은 수준(더 추상적인)으로 요약합니다.\n",
    "\n",
    "이 과정은 재귀적으로 수행되어, 원본 문서(`leafs`)에서 더 추상적인 요약으로 이어지는 \"트리\"를 형성합니다.\n",
    "\n",
    "`leafs`는 다음과 같은 문서들로 구성될 수 있습니다.\n",
    "\n",
    "- 단일 문서에서의 텍스트 청크(논문에서 보여준 것처럼)\n",
    "- 전체 문서(아래에서 보여주는 것처럼)\n",
    "\n",
    "이번 튜토리얼에서는 긴 문서(PDF) 에 대해서 RAPTOR 방법론을 적용해 보도록 하겠습니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4aa9e677",
   "metadata": {},
   "source": [
    "## 실습에 활용한 문서\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "_실습을 위해 다운로드 받은 파일을 `data` 폴더로 복사해 주시기 바랍니다_\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac3b0092",
   "metadata": {},
   "source": [
    "## 환경 설정"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d2bfd93",
   "metadata": {},
   "source": [
    "**추가 패키지 설치**\n",
    "\n",
    "아래 주석을 해제하고 실행하여 추가 패키지를 설치 후 진행해 주세요."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ad741a5e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install -U umap-learn"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a8b1b8d",
   "metadata": {},
   "source": [
    "상단의 **restart** 버튼을 눌러 재시작 한 뒤 다시 처음부터 진행해 주세요."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "45420489",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "import warnings\n",
    "\n",
    "# 경고 메시지 무시\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "572c2efd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "LangSmith 추적을 시작합니다.\n",
      "[프로젝트명]\n",
      "CH12-RAPTOR\n"
     ]
    }
   ],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH12-RAPTOR\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "792c9f09",
   "metadata": {},
   "source": [
    "## 데이터 전처리\n",
    "\n",
    "`doc`은 PDF 파일입니다. 토큰 수는 100 토큰 미만에서 10,000 토큰 이상까지 다양합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55ba7426",
   "metadata": {},
   "source": [
    "웹 문서에서 텍스트 데이터를 추출하고, 텍스트의 토큰 수를 계산하여 히스토그램으로 시각화합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "1d3506db",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "문서의 페이지수: 23\n"
     ]
    },
    {
     "data": {
      "image/png": "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VMnZd+eddyoyMtJlrpLUVprXLwCUN3xUDwB8wBdffKFvvvlGL730kurVq6fNmzerS5cu6tmzp6ZNm+byRfqLff/99+rRo4eWLFmi+fPnq2fPnsrMzNTQoUM1Z84cbdy4UR9//LF+//13RUZG6s4779TatWv1xBNPuKXuKVOmqGnTpnrqqafUqVMnzZ8/X6mpqQoPD1fPnj114403Kjo6WtK5Px57//33a+nSpXrjjTd07NgxDRw4UPXr19e9997rvMKxfft2JSYmavLkybriiit07Ngx9e3bt8AQWFx79uxRenq6hgwZoszMTJ06dUrff/+9y5WYgnTv3l2NGzdWYGCgwsLC1LlzZ8XGxmrv3r26++67dfr06UL3HT9+vDp06KDFixdr7969ql27th599FHt379fa9asuaS6CnPs2DGtWbNG8fHxCgsL0/Dhw7Vr1y69++67zjHvvfeeevfurWXLlunTTz/Vtddeq/79++f72GFJaivt6xcAyhuv39qPRqPRKlor6A/ghoaGmtdee80cOHDAnD592uzcubPYfwD3rrvuMjk5OWbevHnO23p37NjRLF261KSnp5usrCyza9cu8/7775vWrVs797vwB3D/+BwF/THXotpf//pXs2zZMnPkyBGTk5Nj7Ha7mTdvnunQoYPLuAt/APfYsWMmKyvLrF+/Pt8fwL0w7quvvjIOh8McOnTIvPjiiyYmJqbA25Fv3bo13/4F/YHWu+66y/zyyy8mJyfH8tbkF25HfkF2drY5ePCgWb58uXniiSdcbvld2Dnr1KmTWbBggTlw4IDJzs42Bw4cMHPnzjUNGjQoVl2FHduFbQXdjvy+++4zL730kklNTTWnTp0yX3zxhbn66qvz7f/kk0+a/fv3G4fDYb777jvTunXrfHMWVVtB5/dSXr9S4bdJp9FoNF9pfuf/AwAAAABQCL7jBAAAAAAWCE4AAAAAYIHgBAAAAAAWCE4AAAAAYIHgBAAAAAAWCE4AAAAAYKFC/gHcOnXqKDMz09tlAAAAAPAym82mgwcPWo6rcMGpTp06stvt3i4DAAAAgI+oW7euZXiqcMHpwpWmunXrctUJAAAAqMBsNpvsdnuxckGFC04XZGZmEpwAAAAAFAs3hwAAAAAACwQnAAAAALBAcAIAAAAACwQnAAAAALBAcAIAAAAACwQnAAAAALBAcAIA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MerRo0eR4zt27ChjTL4WFhZWRhUDAAAAqIi8GpxCQ0O1efNmPfbYYyXar2HDhgoPD3e2w4cPe6hCAAAAAJACvfnky5Yt07Jly0q83+HDh5WRkeGBigAAAAAgP68Gp9LatGmTgoOD9csvv2jixIlat25doWODgoIUHBzsfGyz2SRJISEhys3N9XitxVNZUsD55k4B5+cOcfO8AAAAF/PUexnp3NtVT83vybk9PX95rt133qOGhBS/hnIVnA4dOqRHHnlEP/30k4KDgzV48GD997//1Y033qiff/65wH3Gjh2riRMn5uv/7LPPlJeX5+GKiytYUj1JQW6eN0fS9ZJOu3leAACAi3nqvYwkNZHUQVKUB+b35Nyenr881+4771EDAoofCv0kGc+VUnzGGPXs2VOff/55ifb773//q3379mnAgAEFbi/oipPdbldYWJgyMzMvqWb3iZAUd/5fdzokadL5fwEAADzFU+9lJGmrpHhJozwwvyfn9vT85bl233mParPZlJaWpmrVqllmg3J1xakgP/zwg9q3b1/o9pycHOXk5OTrdzgccjgcniytBLIl5Z1v7pR3fm5fOU4AAHB58tR7GUnK9eD8npzb0/OX59p95z1qYGDx41C5/ztOrVq10qFD3k+rAAAAAC5fXr3iFBoaqgYNGjgf169fXy1bttSxY8e0f/9+TZo0SXXr1tXAgQMlScOGDVNycrK2bdumypUra/DgwercubO6dOnirUMAAAAAUAF4NTi1bdtW//3vf52Pp02bJkn64IMP9NBDDykiIkKRkZHO7UFBQXrttddUt25dZWVlacuWLbr99ttd5gAAAAAAd/NqcFq9erX8/PwK3f7QQw+5PJ4yZYqmTJni6bIAAAAAwEW5/44TAAAAAHgawQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMACwQkAAAAALBCcAAAAAMAC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paWlqVq1apbZwKtXnI4cOaLc3FyFhYW59IeFhSk1NbVYc+Tm5urnn39WgwYNCtyek5OjnJycfP0Oh8OH7saXLSnvfHOnXA/OnXd+bl85hwAuT576GSbxc6wonjzv5f13U3mu3ZPK62vGk3N7ev7yXLvvvN4DA4sfh7x6c4gzZ85ow4YNiomJcfb5+fkpJiZGiYmJxZrD399fzZs316FD3k+sAAAAAC5PXr3iJElTp05VQkKCfvrpJ/3www8aPny4QkNDFR8fL0lKSEiQ3W5XXFycJGncuHFav369du/erRo1amjUqFGKiorSe++9583DAAAAAHAZ83pw+vTTT3XVVVfp+eefV3h4uDZt2qRu3bo5bzEeGRmps2fPOsfXrFlT7777rsLDw5Wenq4NGzbo5ptvVlJSkrcOAQAAAMBlzuvBSZJmzpypmTNnFritU6dOLo9HjBihESNGlEVZAAAAACDJB/4ALgAAAAD4OoITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFggOAEAAACABYITAAAAAFjwieD06KOPKjk5WQ6HQ+vXr1e7du2KHN+rVy8lJSXJ4XBoy5Yt6t69exlVCgAAAKAi8npw6tOnj6ZOnarnnntOrVu31ubNm7V8+XJdddVVBY6Pjo7WvHnzNGvWLN1www1auHChFi5cqKZNm5Zx5QAAAAAqCq8HpxEjRujdd9/VBx98oKSkJA0ZMkRZWVkaNGhQgeOHDRumZcuW6dVXX9WOHTs0fvx4bdy4UY8//ngZVw4AAACgogj05pNXqlRJbdq00eTJk519xhitWLFC0dHRBe4THR2t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",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from langchain_community.document_loaders import PDFPlumberLoader\n",
    "import tiktoken\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "\n",
    "# 토큰 수 계산\n",
    "def num_tokens_from_string(string: str, encoding_name: str):\n",
    "    encoding = tiktoken.get_encoding(encoding_name)\n",
    "    num_tokens = len(encoding.encode(string))\n",
    "    return num_tokens\n",
    "\n",
    "\n",
    "# 문서 로드(Load Documents)\n",
    "loader = PDFPlumberLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 페이지수: {len(docs)}\")\n",
    "\n",
    "# 문서 텍스트\n",
    "docs_texts = [d.page_content for d in docs]\n",
    "\n",
    "# 각 문서에 대한 토큰 수 계산\n",
    "counts = [num_tokens_from_string(d, \"cl100k_base\") for d in docs_texts]\n",
    "\n",
    "# 토큰 수의 히스토그램을 그립니다.\n",
    "plt.figure(figsize=(10, 6))\n",
    "plt.hist(counts, bins=30, color=\"blue\", edgecolor=\"black\", alpha=0.7)\n",
    "plt.title(\"Token Count Distribution\")\n",
    "plt.xlabel(\"Token Count\")\n",
    "plt.ylabel(\"Frequency\")\n",
    "plt.grid(axis=\"y\", alpha=0.75)\n",
    "\n",
    "# 히스토그램을 표시합니다.\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66c9d8ad",
   "metadata": {},
   "source": [
    "문서 텍스트를 정렬합니다. 이때 메타데이터의 `source` 를 기준으로 정렬한 뒤, 모든 문서를 연결합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ff0a783a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "전체 토큰 수: 24131\n"
     ]
    }
   ],
   "source": [
    "# 문서를 출처 메타데이터 기준으로 정렬합니다.\n",
    "d_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\n",
    "d_reversed = list(reversed(d_sorted))\n",
    "\n",
    "# 역순으로 배열된 문서의 내용을 연결합니다.\n",
    "concatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n",
    "    [doc.page_content for doc in d_reversed]\n",
    ")\n",
    "\n",
    "print(\n",
    "    \"전체 토큰 수: %s\"  # 모든 문맥에서의 토큰 수를 출력합니다.\n",
    "    % num_tokens_from_string(concatenated_content, \"cl100k_base\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50a33263",
   "metadata": {},
   "source": [
    "`RecursiveCharacterTextSplitter`를 사용하여 텍스트를 분할합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "8982e516",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 텍스트 분할을 위한 코드\n",
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "\n",
    "# 기준 토큰수\n",
    "chunk_size = 100\n",
    "\n",
    "# 텍스트 분할기 초기화\n",
    "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
    "    chunk_size=chunk_size, chunk_overlap=0\n",
    ")\n",
    "\n",
    "# 주어진 텍스트를 분할\n",
    "texts_split = text_splitter.split_text(concatenated_content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dcc0990c",
   "metadata": {},
   "source": [
    "다음으로는 분할된 chunk 들을 임베딩하여 vector store 에 저장합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b7d18ea6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import OpenAIEmbeddings\n",
    "from langchain.embeddings import CacheBackedEmbeddings\n",
    "from langchain.storage import LocalFileStore\n",
    "\n",
    "# cache 저장 경로 지정\n",
    "store = LocalFileStore(\"./cache/\")\n",
    "\n",
    "# embeddings 인스턴스를 생성\n",
    "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\", disallowed_special=())\n",
    "\n",
    "# CacheBackedEmbeddings 인스턴스를 생성\n",
    "cached_embeddings = CacheBackedEmbeddings.from_bytes_store(\n",
    "    embeddings, store, namespace=embeddings.model\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24bcae7c",
   "metadata": {},
   "source": [
    "## 모델 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "a48631ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import stream_response\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "\n",
    "# llm 모델 초기화\n",
    "llm = ChatOpenAI(\n",
    "    model=\"gpt-4.1-mini\",\n",
    "    temperature=0,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b89741a7",
   "metadata": {},
   "source": [
    "## 트리 구축\n",
    "\n",
    "트리 구축에서의 클러스터링 접근 방식에 대한 주요 개요입니다.\n",
    "\n",
    "**GMM (가우시안 혼합 모델)**\n",
    "\n",
    "- 다양한 클러스터에 걸쳐 데이터 포인트의 분포를 모델링합니다.\n",
    "- 모델의 베이지안 정보 기준(BIC)을 평가하여 최적의 클러스터 수를 결정합니다.\n",
    "\n",
    "**UMAP (Uniform Manifold Approximation and Projection)**\n",
    "\n",
    "- 클러스터링을 지원합니다.\n",
    "- 고차원 데이터의 차원을 축소합니다.\n",
    "- UMAP은 데이터 포인트의 유사성에 기반하여 자연스러운 그룹화를 강조하는 데 도움을 줍니다.\n",
    "\n",
    "**지역 및 전역 클러스터링**\n",
    "\n",
    "- 데이터를 저차원으로 차원 축소하여 클러스터링을 수행합니다.\n",
    "\n",
    "**임계값 설정**\n",
    "\n",
    "- GMM의 맥락에서 클러스터 멤버십을 결정하기 위해 적용됩니다.\n",
    "- 확률 분포를 기반으로 합니다(데이터 포인트를 ≥ 1 클러스터에 할당).\n",
    "\n",
    "---\n",
    "\n",
    "GMM 및 임계값 설정에 대한 코드는 아래 두 출처에서 언급된 Sarthi et al의 것입니다. \n",
    "\n",
    "**참조**\n",
    "\n",
    "- [원본 저장소](https://github.com/parthsarthi03/raptor/blob/master/raptor/cluster_tree_builder.py)\n",
    "- [소소한 조정](https://github.com/run-llama/llama_index/blob/main/llama-index-packs/llama-index-packs-raptor/llama_index/packs/raptor/clustering.py)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6bd9d71",
   "metadata": {},
   "source": [
    "### 차원 축소\n",
    "\n",
    "`global_cluster_embeddings`\n",
    "\n",
    "- 입력된 임베딩 벡터를 전역적으로 차원 축소하기 위해 UMAP을 적용합니다. 전역적으로 차원을 축소한 결과물을 얻어 추후 클러스터링에 활용합니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- n_neighbors: UMAP에 사용될 이웃(neighbor) 수를 정합니다. 데이터 포인트 하나를 이해할 때 주변 데이터 포인트 개수를 나타냅니다. 입력이 없으면 데이터 개수에 따라 자동으로 계산합니다.\n",
    "- umap.UMAP(...)를 사용하여, 고차원 임베딩을 dim 차원으로 축소합니다.\n",
    "- 축소된 벡터들은 전역적(global)인 구조 파악에 도움이 되는 저차원 표현입니다.\n",
    "\n",
    "---\n",
    "\n",
    "`local_cluster_embeddings`\n",
    "\n",
    "- 선택한 데이터 부분집합에 대해 로컬(국소적) 차원 축소를 수행합니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- 글로벌 차원 축소와 유사하지만, 로컬 차원 축소는 이미 한 번 전역 클러스터링을 통해 추출한 특정 그룹(글로벌 클러스터) 내 데이터에 대해 다시 UMAP을 적용합니다.\n",
    "- 이 과정은 전역적으로 파악된 큰 구조 안에서 더 세밀한 클러스터 구조를 파악하는 데 도움이 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "2970a692",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Dict, List, Optional, Tuple\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import umap\n",
    "from langchain.prompts import ChatPromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from sklearn.mixture import GaussianMixture\n",
    "\n",
    "RANDOM_SEED = 42  # 재현성을 위한 고정된 시드 값\n",
    "\n",
    "\n",
    "def global_cluster_embeddings(\n",
    "    embeddings: np.ndarray,\n",
    "    dim: int,\n",
    "    n_neighbors: Optional[int] = None,\n",
    "    metric: str = \"cosine\",\n",
    ") -> np.ndarray:\n",
    "    \"\"\"전역적으로 임베딩 벡터의 차원을 축소하는 함수입니다.\n",
    "\n",
    "    Args:\n",
    "        embeddings (np.ndarray): 차원을 축소할 임베딩 벡터들\n",
    "        dim (int): 축소할 차원의 수\n",
    "        n_neighbors (Optional[int], optional): UMAP에서 사용할 이웃의 수. 기본값은 None으로, 이 경우 데이터 크기에 따라 자동 계산됨\n",
    "        metric (str, optional): 거리 계산에 사용할 메트릭. 기본값은 \"cosine\"\n",
    "\n",
    "    Returns:\n",
    "        np.ndarray: 차원이 축소된 임베딩 벡터들\n",
    "    \"\"\"\n",
    "    # 이웃 수 계산\n",
    "    if n_neighbors is None:\n",
    "        n_neighbors = int((len(embeddings) - 1) ** 0.5)\n",
    "\n",
    "    # UMAP 적용\n",
    "    return umap.UMAP(\n",
    "        n_neighbors=n_neighbors, n_components=dim, metric=metric\n",
    "    ).fit_transform(embeddings)\n",
    "\n",
    "\n",
    "def local_cluster_embeddings(\n",
    "    embeddings: np.ndarray, dim: int, num_neighbors: int = 10, metric: str = \"cosine\"\n",
    ") -> np.ndarray:\n",
    "    \"\"\"로컬(국소적)하게 임베딩 벡터의 차원을 축소하는 함수입니다.\n",
    "\n",
    "    Args:\n",
    "        embeddings (np.ndarray): 차원을 축소할 임베딩 벡터들\n",
    "        dim (int): 축소할 차원의 수\n",
    "        num_neighbors (int, optional): UMAP에서 사용할 이웃의 수. 기본값은 10\n",
    "        metric (str, optional): 거리 계산에 사용할 메트릭. 기본값은 \"cosine\"\n",
    "\n",
    "    Returns:\n",
    "        np.ndarray: 차원이 축소된 임베딩 벡터들\n",
    "    \"\"\"\n",
    "    # UMAP 적용\n",
    "    return umap.UMAP(\n",
    "        n_neighbors=num_neighbors, n_components=dim, metric=metric\n",
    "    ).fit_transform(embeddings)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48430adc",
   "metadata": {},
   "source": [
    "### 최적의 클러스터 수 계산"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab30f112",
   "metadata": {},
   "source": [
    "`get_optimal_clusters` \n",
    "\n",
    "- 주어진 임베딩 데이터에 대해 가장 적절한 클러스터 수를 BIC 점수를 기반으로 결정합니다.\n",
    "- GMM과 BIC를 활용해 클러스터 개수를 자동으로 결정하므로, 사전에 클러스터 수를 지정할 필요가 없습니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- 가능한 클러스터 수(1 ~ max_clusters 사이)를 순회하며 각 클러스터 개수로 GMM을 학습합니다.\n",
    "- 각 GMM에 대해 BIC 점수를 계산한 뒤 리스트에 저장합니다.\n",
    "- BIC 점수가 가장 낮은(가장 좋은 성능을 보이는) 클러스터 개수를 선택하여 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ade041a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_optimal_clusters(\n",
    "    embeddings: np.ndarray, max_clusters: int = 50, random_state: int = RANDOM_SEED\n",
    ") -> int:\n",
    "    \"\"\"BIC 점수를 기반으로 최적의 클러스터 수를 찾는 함수입니다.\n",
    "\n",
    "    Args:\n",
    "        embeddings (np.ndarray): 클러스터링할 임베딩 벡터들\n",
    "        max_clusters (int, optional): 탐색할 최대 클러스터 수. 기본값은 50\n",
    "        random_state (int, optional): 난수 생성을 위한 시드값. 기본값은 RANDOM_SEED\n",
    "\n",
    "    Returns:\n",
    "        int: BIC 점수가 가장 낮은(최적의) 클러스터 수\n",
    "    \"\"\"\n",
    "    # 최대 클러스터 수와 임베딩의 길이 중 작은 값을 최대 클러스터 수로 설정\n",
    "    max_clusters = min(max_clusters, len(embeddings))\n",
    "    # 1부터 최대 클러스터 수까지의 범위를 생성\n",
    "    n_clusters = np.arange(1, max_clusters)\n",
    "\n",
    "    # BIC 점수를 저장할 리스트\n",
    "    bics = []\n",
    "    for n in n_clusters:\n",
    "        gm = GaussianMixture(n_components=n, random_state=random_state)\n",
    "        gm.fit(embeddings)\n",
    "        # 학습된 모델의 BIC 점수를 리스트에 추가\n",
    "        bics.append(gm.bic(embeddings))\n",
    "\n",
    "    # BIC 점수가 가장 낮은 클러스터 수를 반환\n",
    "    return n_clusters[np.argmin(bics)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82573eae",
   "metadata": {},
   "source": [
    "### 클러스터링 수행"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c27119b8",
   "metadata": {},
   "source": [
    "`GMM_cluster` \n",
    "\n",
    "- GMM을 이용해 주어진 임베딩에 대해 클러스터를 할당합니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- `get_optimal_clusters` 를 통해 최적의 클러스터 수를 찾습니다.\n",
    "- `GaussianMixture` 모델을 해당 클러스터 수로 학습합니다.\n",
    "- 각 데이터 포인트가 각 클러스터에 속할 확률(predict_proba)을 구합니다.\n",
    "- 주어진 threshold를 바탕으로, 확률이 임계값을 초과하는 클러스터만 레이블로 할당합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "f86a6d1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def GMM_cluster(embeddings: np.ndarray, threshold: float, random_state: int = 0):\n",
    "    # 최적의 클러스터 수 산정\n",
    "    n_clusters = get_optimal_clusters(embeddings)\n",
    "\n",
    "    # 가우시안 혼합 모델을 초기화\n",
    "    gm = GaussianMixture(n_components=n_clusters, random_state=random_state)\n",
    "    gm.fit(embeddings)\n",
    "\n",
    "    # 임베딩이 각 클러스터에 속할 확률을 예측\n",
    "    probs = gm.predict_proba(embeddings)\n",
    "\n",
    "    # 임계값을 초과하는 확률을 가진 클러스터를 레이블로 선택\n",
    "    labels = [np.where(prob > threshold)[0] for prob in probs]\n",
    "\n",
    "    # 레이블과 클러스터 수를 반환\n",
    "    return labels, n_clusters"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a80d9bd6",
   "metadata": {},
   "source": [
    "`perform_clustering` \n",
    "\n",
    "- 전역 차원 축소, 전역 클러스터링, 이후 로컬 차원 축소 및 로컬 클러스터링까지 전체 클러스터링 파이프라인을 수행하는 핵심 함수입니다.\n",
    "- 이전의 과정을 하나의 파이프라인으로 만들어 종합하는 역할을 수행합니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- 입력된 embeddings가 충분한지 확인(적은 경우 단순 할당).\n",
    "- 전역 차원 축소: `global_cluster_embeddings` 로 전체 임베딩에 대해 UMAP 적용.\n",
    "- 전역 클러스터링: 전역 차원 축소 결과에 대해 `GMM_cluster` 를 사용하여 전역 클러스터 형성.\n",
    "- 각 전역 클러스터에 속하는 데이터만 추출 -> 해당 집합에 대해 로컬 차원 축소(`local_cluster_embeddings`) 수행.\n",
    "- 로컬 차원 축소 결과에 대해 다시 `GMM_cluster` 로 로컬 클러스터링 수행.\n",
    "- 최종적으로, 각 데이터 포인트에 대해서 전역 및 로컬 클러스터 레이블을 함께 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "32e97d75",
   "metadata": {},
   "outputs": [],
   "source": [
    "def perform_clustering(\n",
    "    embeddings: np.ndarray,\n",
    "    dim: int,\n",
    "    threshold: float,\n",
    ") -> List[np.ndarray]:\n",
    "    \"\"\"\n",
    "    임베딩에 대해 계층적 클러스터링을 수행하는 함수입니다.\n",
    "\n",
    "    전역 차원 축소와 클러스터링을 먼저 수행한 후, 각 전역 클러스터 내에서\n",
    "    로컬 차원 축소와 클러스터링을 수행합니다.\n",
    "\n",
    "    Args:\n",
    "        embeddings (np.ndarray): 클러스터링할 임베딩 벡터들\n",
    "        dim (int): 차원 축소 시 목표 차원 수\n",
    "        threshold (float): GMM 클러스터링에서 사용할 확률 임계값\n",
    "\n",
    "    Returns:\n",
    "        List[np.ndarray]: 각 데이터 포인트에 대한 로컬 클러스터 레이블 리스트.\n",
    "                         각 레이블은 해당 데이터 포인트가 속한 로컬 클러스터의 인덱스를 담은 numpy 배열입니다.\n",
    "    \"\"\"\n",
    "\n",
    "    if len(embeddings) <= dim + 1:\n",
    "        # 데이터가 충분하지 않을 때 클러스터링을 피합니다.\n",
    "        return [np.array([0]) for _ in range(len(embeddings))]\n",
    "\n",
    "    # 글로벌 차원 축소\n",
    "    reduced_embeddings_global = global_cluster_embeddings(embeddings, dim)\n",
    "\n",
    "    # 글로벌 클러스터링\n",
    "    global_clusters, n_global_clusters = GMM_cluster(\n",
    "        reduced_embeddings_global, threshold\n",
    "    )\n",
    "\n",
    "    # 로컬 클러스터링을 위한 초기화\n",
    "    all_local_clusters = [np.array([]) for _ in range(len(embeddings))]\n",
    "    total_clusters = 0\n",
    "\n",
    "    # 각 글로벌 클러스터를 순회하며 로컬 클러스터링 수행\n",
    "    for i in range(n_global_clusters):\n",
    "        # 현재 글로벌 클러스터에 속하는 임베딩 추출\n",
    "        global_cluster_embeddings_ = embeddings[\n",
    "            np.array([i in gc for gc in global_clusters])\n",
    "        ]\n",
    "\n",
    "        if len(global_cluster_embeddings_) == 0:\n",
    "            continue\n",
    "        if len(global_cluster_embeddings_) <= dim + 1:\n",
    "            # 작은 클러스터는 직접 할당으로 처리\n",
    "            local_clusters = [np.array([0]) for _ in global_cluster_embeddings_]\n",
    "            n_local_clusters = 1\n",
    "        else:\n",
    "            # 로컬 차원 축소 및 클러스터링\n",
    "            reduced_embeddings_local = local_cluster_embeddings(\n",
    "                global_cluster_embeddings_, dim\n",
    "            )\n",
    "            local_clusters, n_local_clusters = GMM_cluster(\n",
    "                reduced_embeddings_local, threshold\n",
    "            )\n",
    "\n",
    "        # 로컬 클러스터 ID 할당, 이미 처리된 총 클러스터 수를 조정\n",
    "        for j in range(n_local_clusters):\n",
    "            local_cluster_embeddings_ = global_cluster_embeddings_[\n",
    "                np.array([j in lc for lc in local_clusters])\n",
    "            ]\n",
    "            indices = np.where(\n",
    "                (embeddings == local_cluster_embeddings_[:, None]).all(-1)\n",
    "            )[1]\n",
    "            for idx in indices:\n",
    "                all_local_clusters[idx] = np.append(\n",
    "                    all_local_clusters[idx], j + total_clusters\n",
    "                )\n",
    "\n",
    "        total_clusters += n_local_clusters\n",
    "\n",
    "    return all_local_clusters"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e051d771",
   "metadata": {},
   "source": [
    "주어진 텍스트 리스트를 임베딩 모델을 이용해 벡터로 변환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "175963c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def embed(texts):\n",
    "    \"\"\"\n",
    "    주어진 텍스트 리스트를 임베딩 벡터로 변환합니다.\n",
    "\n",
    "    Args:\n",
    "        texts (List[str]): 임베딩할 텍스트 리스트\n",
    "\n",
    "    Returns:\n",
    "        np.ndarray: 텍스트의 임베딩 벡터를 포함하는 numpy 배열\n",
    "                   shape은 (텍스트 개수, 임베딩 차원)입니다.\n",
    "    \"\"\"\n",
    "    text_embeddings = embeddings.embed_documents(texts)\n",
    "\n",
    "    # 임베딩을 numpy 배열로 변환\n",
    "    text_embeddings_np = np.array(text_embeddings)\n",
    "    return text_embeddings_np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a74db77a",
   "metadata": {},
   "source": [
    "`embed_cluster_texts` \n",
    "\n",
    "- 텍스트 리스트를 임베딩하고, 위에서 정의한 클러스터링 절차를 수행한 뒤 결과를 데이터프레임 형태로 반환합니다\n",
    "\n",
    "**과정**\n",
    "\n",
    "- embed 함수를 통해 텍스트를 임베딩합니다.\n",
    "- perform_clustering를 호출하여 클러스터 라벨을 얻습니다.\n",
    "- 원본 텍스트, 임베딩, 클러스터 정보를 하나의 DataFrame에 통합하여 반환합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "351f42b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def embed_cluster_texts(texts):\n",
    "    # 임베딩 생성\n",
    "    text_embeddings_np = embed(texts)\n",
    "    # 클러스터링 수행\n",
    "    cluster_labels = perform_clustering(text_embeddings_np, 10, 0.1)\n",
    "    # 결과를 저장할 DataFrame 초기화\n",
    "    df = pd.DataFrame()\n",
    "    # 원본 텍스트 저장\n",
    "    df[\"text\"] = texts\n",
    "    # DataFrame에 리스트로 임베딩 저장\n",
    "    df[\"embd\"] = list(text_embeddings_np)\n",
    "    # 클러스터 라벨 저장\n",
    "    df[\"cluster\"] = cluster_labels\n",
    "    return df"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a4a3ecd",
   "metadata": {},
   "source": [
    "`fmt_txt` 함수는 `pandas`의 `DataFrame`에서 텍스트 문서를 단일 문자열로 포맷팅합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "903883eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def fmt_txt(df: pd.DataFrame) -> str:\n",
    "    \"\"\"\n",
    "    주어진 DataFrame에서 텍스트 문서를 단일 문자열로 포맷팅하는 함수입니다.\n",
    "\n",
    "    Args:\n",
    "        df (pd.DataFrame): 포맷팅할 텍스트 문서를 포함한 DataFrame\n",
    "\n",
    "    Returns:\n",
    "        str: 텍스트 문서들을 특정 구분자로 결합한 단일 문자열\n",
    "    \"\"\"\n",
    "    unique_txt = df[\"text\"].tolist()\n",
    "    return \"--- --- \\n --- --- \".join(unique_txt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecc49b18",
   "metadata": {},
   "source": [
    "`embed_cluster_summarize_texts` \n",
    "\n",
    "- 텍스트 리스트에 대해 임베딩 → 클러스터링 → 요약 까지 전체 프로세스를 수행합니다.\n",
    "\n",
    "**과정**\n",
    "\n",
    "- 임베딩 & 클러스터링: `embed_cluster_texts` 함수를 이용해 입력된 텍스트를 임베딩하고 클러스터링한 결과를 `df_clusters` 로 얻습니다. 이 `df_clusters` 는 각 문서와 그 문서를 할당받은 (하나 이상일 수 있는) 클러스터를 가지고 있습니다.\n",
    "  \n",
    "- 클러스터 할당 확장: 어떤 문서가 여러 클러스터에 속할 수 있으므로, 이를 행 단위로 '문서-클러스터' 페어로 확장한 `expanded_df` 를 만듭니다. 이렇게 하면 이후 처리(특히 요약 단계)에서 각 클러스터별로 문서를 쉽게 그룹화할 수 있습니다.\n",
    "\n",
    "- LLM(대형 언어 모델)을 이용한 요약: 각 클러스터에 속한 문서들의 텍스트를 하나의 문자열로 합친 뒤(`fmt_txt` 사용), 프롬프트 템플릿을 통해 LLM에 전달합니다. LLM은 해당 클러스터에 대한 요약 문장을 생성합니다.\n",
    "\n",
    "- 요약 결과 정리: 클러스터별 요약 결과를 `df_summary` DataFrame에 저장합니다. 여기에는 summaries(요약문), level(입력 파라미터로 받은 처리 수준), cluster(클러스터 식별자)가 포함됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "c242240e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def embed_cluster_summarize_texts(\n",
    "    texts: List[str], level: int\n",
    ") -> Tuple[pd.DataFrame, pd.DataFrame]:\n",
    "    \"\"\"\n",
    "    텍스트 목록에 대해 임베딩, 클러스터링 및 요약을 수행합니다. 이 함수는 먼저 텍스트에 대한 임베딩을 생성하고,\n",
    "    유사성을 기반으로 클러스터링을 수행한 다음, 클러스터 할당을 확장하여 처리를 용이하게 하고 각 클러스터 내의 내용을 요약합니다.\n",
    "\n",
    "    매개변수:\n",
    "    - texts: 처리할 텍스트 문서 목록입니다.\n",
    "    - level: 처리의 깊이나 세부 사항을 정의할 수 있는 정수 매개변수입니다.\n",
    "\n",
    "    반환값:\n",
    "    - 두 개의 데이터프레임을 포함하는 튜플:\n",
    "      1. 첫 번째 데이터프레임(`df_clusters`)은 원본 텍스트, 그들의 임베딩, 그리고 클러스터 할당을 포함합니다.\n",
    "      2. 두 번째 데이터프레임(`df_summary`)은 각 클러스터에 대한 요약, 지정된 세부 수준, 그리고 클러스터 식별자를 포함합니다.\n",
    "    \"\"\"\n",
    "\n",
    "    # 텍스트를 임베딩하고 클러스터링하여 'text', 'embd', 'cluster' 열이 있는 데이터프레임을 생성합니다.\n",
    "    df_clusters = embed_cluster_texts(texts)\n",
    "\n",
    "    # 클러스터를 쉽게 조작하기 위해 데이터프레임을 확장할 준비를 합니다.\n",
    "    expanded_list = []\n",
    "\n",
    "    # 데이터프레임 항목을 문서-클러스터 쌍으로 확장하여 처리를 간단하게 합니다.\n",
    "    for index, row in df_clusters.iterrows():\n",
    "        for cluster in row[\"cluster\"]:\n",
    "            expanded_list.append(\n",
    "                {\"text\": row[\"text\"], \"embd\": row[\"embd\"], \"cluster\": cluster}\n",
    "            )\n",
    "\n",
    "    # 확장된 목록에서 새 데이터프레임을 생성합니다.\n",
    "    expanded_df = pd.DataFrame(expanded_list)\n",
    "\n",
    "    # 처리를 위해 고유한 클러스터 식별자를 검색합니다.\n",
    "    all_clusters = expanded_df[\"cluster\"].unique()\n",
    "\n",
    "    print(f\"--Generated {len(all_clusters)} clusters--\")\n",
    "\n",
    "    # 요약\n",
    "    template = \"\"\"여기 LangChain 표현 언어 문서의 하위 집합이 있습니다.\n",
    "    \n",
    "    LangChain 표현 언어는 LangChain에서 체인을 구성하는 방법을 제공합니다.\n",
    "    \n",
    "    제공된 문서의 자세한 요약을 제공하십시오.\n",
    "    \n",
    "    문서:\n",
    "    {context}\n",
    "    \"\"\"\n",
    "    prompt = ChatPromptTemplate.from_template(template)\n",
    "    chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "    # 각 클러스터 내의 텍스트를 요약을 위해 포맷팅합니다.\n",
    "    summaries = []\n",
    "    for i in all_clusters:\n",
    "        df_cluster = expanded_df[expanded_df[\"cluster\"] == i]\n",
    "        formatted_txt = fmt_txt(df_cluster)\n",
    "        summaries.append(chain.invoke({\"context\": formatted_txt}))\n",
    "\n",
    "    # 요약, 해당 클러스터 및 레벨을 저장할 데이터프레임을 생성합니다.\n",
    "    df_summary = pd.DataFrame(\n",
    "        {\n",
    "            \"summaries\": summaries,\n",
    "            \"level\": [level] * len(summaries),\n",
    "            \"cluster\": list(all_clusters),\n",
    "        }\n",
    "    )\n",
    "\n",
    "    return df_clusters, df_summary"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95191d59",
   "metadata": {},
   "source": [
    "`recursive_embed_cluster_summarize`\n",
    "\n",
    "- 텍스트 데이터에 대해 여러 \"단계(Level)\"에 걸쳐 클러스터링과 요약을 반복적으로 수행합니다.\n",
    "- 처음에는 원본 텍스트에 대해 클러스터링 및 요약을 수행한 뒤, 각 클러스터 요약을 다음 단계의 입력 텍스트로 삼아 다시 임베딩 → 클러스터링 → 요약을 반복합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "c417dece",
   "metadata": {},
   "outputs": [],
   "source": [
    "def recursive_embed_cluster_summarize(\n",
    "    texts: List[str], level: int = 1, n_levels: int = 3\n",
    ") -> Dict[int, Tuple[pd.DataFrame, pd.DataFrame]]:\n",
    "    # 각 레벨에서의 결과를 저장할 사전\n",
    "    results = {}\n",
    "\n",
    "    # 현재 레벨에 대해 임베딩, 클러스터링, 요약 수행\n",
    "    df_clusters, df_summary = embed_cluster_summarize_texts(texts, level)\n",
    "\n",
    "    # 현재 레벨의 결과 저장\n",
    "    results[level] = (df_clusters, df_summary)\n",
    "\n",
    "    # 추가 재귀가 가능하고 의미가 있는지 결정\n",
    "    unique_clusters = df_summary[\"cluster\"].nunique()\n",
    "\n",
    "    # 현재 레벨이 최대 레벨보다 낮고, 유니크한 클러스터가 1개 이상인 경우\n",
    "    if level < n_levels and unique_clusters > 1:\n",
    "        # 다음 레벨의 재귀 입력 텍스트로 요약 사용\n",
    "        new_texts = df_summary[\"summaries\"].tolist()\n",
    "        next_level_results = recursive_embed_cluster_summarize(\n",
    "            new_texts, level + 1, n_levels\n",
    "        )\n",
    "\n",
    "        # 다음 레벨의 결과를 현재 결과 사전에 병합\n",
    "        results.update(next_level_results)\n",
    "\n",
    "    return results"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8db60197",
   "metadata": {},
   "source": [
    "전체 문서의 개수를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "2de5a73a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "770"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 전체 문서의 개수\n",
    "len(texts_split)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87ab3e94",
   "metadata": {},
   "source": [
    "이제 `recursive_embed_cluster_summarize` 함수를 호출하여 트리 구축을 시작합니다.\n",
    "\n",
    "- `level=1` 은 첫 번째 단계의 클러스터링 및 요약부터 시작한다는 의미입니다.\n",
    "- `n_levels=3` 은 최대 세 단계까지(조건이 맞는 한) 클러스터링과 요약을 재귀적으로 반복할 수 있다는 뜻입니다.\n",
    "- \n",
    "결과적으로, 원본 텍스트(leaf_texts)는 먼저 level=1에서 요약되고 클러스터링됩니다. 그 결과로 나온 각 클러스터의 요약이 다음 단계의 입력(level=2)이 되고, 이를 다시 요약하여 클러스터링 한 결과가 level=3 단계의 입력이 될 수 있습니다. \n",
    "\n",
    "이 과정을 통해 점차 더 추상적이고 집약된 요약 정보를 얻을 수 있게 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "e429ad5d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--Generated 135 clusters--\n",
      "--Generated 24 clusters--\n",
      "--Generated 3 clusters--\n"
     ]
    }
   ],
   "source": [
    "# 트리 구축\n",
    "leaf_texts = texts_split.copy()\n",
    "\n",
    "# 재귀적으로 임베딩, 클러스터링 및 요약을 수행하여 결과를 얻음\n",
    "results = recursive_embed_cluster_summarize(leaf_texts, level=1, n_levels=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff5156ee",
   "metadata": {},
   "source": [
    "다음으로는 vectorstore를 생성하고 로컬에 저장합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "822f26cf",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['홈페이지 : https://spri.kr/',\n",
       " '보고서와 관련된 문의는 AI정책연구실(jayoo@spri.kr, 031-739-7352)으로 연락주시기 바랍니다.',\n",
       " '---',\n",
       " 'Ⅱ\\n. 주요 행사 일정\\n행사명 행사 주요 개요',\n",
       " '- 미국 소비자기술 협회(CTA)가 주관하는 세계 최대 가전·IT·소',\n",
       " '비재 전시회로 5G, AR&VR, 디지털헬스, 교통·모빌리티 등',\n",
       " '주요 카테고리 중심으로 기업들이 최신의 기술 제품군을 전시',\n",
       " '- CTA 사피로 회장은 가장 주목받는 섹터로 AI를 조명하였으며,',\n",
       " '모든 산업을 포괄한다는 의미에서 ‘올 온(All on)’을 주제로 한\\nCES 2024',\n",
       " '이번 전시에는 500곳 이상의 한국기업 참가 예정\\n기간 장소 홈페이지']"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "leaf_texts[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "fbd18b8a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import FAISS\n",
    "\n",
    "all_texts = leaf_texts.copy()\n",
    "\n",
    "# 레벨을 정렬하여 순회\n",
    "for level in sorted(results.keys()):\n",
    "    # 현재 레벨의 DataFrame에서 요약을 추출\n",
    "    summaries = results[level][1][\"summaries\"].tolist()\n",
    "    # 현재 레벨의 요약을 all_texts에 추가합니다.\n",
    "    all_texts.extend(summaries)\n",
    "\n",
    "# 이제 all_texts를 사용하여 FAISS vectorstore를 구축합니다.\n",
    "vectorstore = FAISS.from_texts(texts=all_texts, embedding=embeddings)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7eea66d",
   "metadata": {},
   "source": [
    "DB 를 로컬에 저장합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "15f67a9f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "DB_INDEX = \"RAPTOR\"\n",
    "\n",
    "# 기존 DB 인덱스가 존재하면 로드하여 vectorstore와 병합한 후 저장합니다.\n",
    "if os.path.exists(DB_INDEX):\n",
    "    local_index = FAISS.load_local(DB_INDEX, embeddings)\n",
    "    local_index.merge_from(vectorstore)\n",
    "    local_index.save_local(DB_INDEX)\n",
    "else:\n",
    "    vectorstore.save_local(folder_path=DB_INDEX)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ad41f05",
   "metadata": {},
   "source": [
    "`vectorstore` 로부터 `retriever`를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "398b4dab",
   "metadata": {},
   "outputs": [],
   "source": [
    "# retriever 생성\n",
    "retriever = vectorstore.as_retriever()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6da0f7e",
   "metadata": {},
   "source": [
    "## RAG 체인 정의\n",
    "\n",
    "이제 생성된 vectorstore를 이용해 RAG 체인을 정의하고 실행하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "a9c26dc6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "\n",
    "# 프롬프트 정의\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"\n",
    "    You are an AI assistant specializing in Question-Answering (QA) tasks within a Retrieval-Augmented Generation (RAG) system. \n",
    "You are given PDF documents. Your primary mission is to answer questions based on provided context.\n",
    "Ensure your response is concise and directly addresses the question without any additional narration.\n",
    "\n",
    "###\n",
    "\n",
    "Your final answer should be written concisely (but include important numerical values, technical terms, jargon, and names).\n",
    "\n",
    "# Steps\n",
    "\n",
    "1. Carefully read and understand the context provided.\n",
    "2. Identify the key information related to the question within the context.\n",
    "3. Formulate a concise answer based on the relevant information.\n",
    "4. Ensure your final answer directly addresses the question.\n",
    "\n",
    "# Output Format:\n",
    "[General introduction of the answer]\n",
    "[Comprehensive answer to the question]\n",
    "\n",
    "###\n",
    "\n",
    "Remember:\n",
    "- It's crucial to base your answer solely on the **PROVIDED CONTEXT**. \n",
    "- DO NOT use any external knowledge or information not present in the given materials.\n",
    "\n",
    "###\n",
    "\n",
    "# Here is the user's QUESTION that you should answer:\n",
    "{question}\n",
    "\n",
    "# Here is the CONTEXT that you should use to answer the question:\n",
    "{context}\n",
    "\n",
    "[Note]\n",
    "- Answer should be written in Korean.\n",
    "\n",
    "# Your final ANSWER to the user's QUESTION:\"\"\"\n",
    ")\n",
    "\n",
    "\n",
    "# 문서 포맷팅\n",
    "def format_docs(docs):\n",
    "    return \"\\n\\n\".join(f\"<document>{doc.page_content}</document>\" for doc in docs)\n",
    "\n",
    "\n",
    "# RAG 체인 정의\n",
    "rag_chain = (\n",
    "    {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52da8b08",
   "metadata": {},
   "source": [
    "[LangSmith 링크](https://smith.langchain.com/public/e5acd315-a662-4f93-aec3-04c80a8bd2a4/r)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "e0efdda7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "주어진 문서들은 법적 및 윤리적 문제, 기술 및 정보 교류, 데이터 분석, 첨단 AI 시스템과 관련된 다양한 주제를 다루고 있습니다.\n",
      "\n",
      "1. **법적 및 윤리적 문제**: 정책입안자와 국제 파트너의 역할, 법적·윤리적 문제의 체계적 접근, 입증 관행, 일관된 법적 프레임워크, 저작권법과 불공정 경쟁, 불법 행위 대처 방안 등이 논의됩니다.\n",
      "\n",
      "2. **기술 및 정보 교류**: 신기술 간 상보성, 정보교류 활성화, 개인 식별 및 공직자 관련 요소, 엔지니어링 및 CRM, 협력과 보안, 이해관계자 협의 및 개정 등이 포함됩니다.\n",
      "\n",
      "3. **데이터 분석**: 데이터 활용과 의사결정, 데이터 투명성, 데이터셋 감사 및 추적, 데이터 생태계 분석, 법적 이슈, 고성능 컴퓨팅의 중요성이 강조됩니다.\n",
      "\n",
      "4. **첨단 AI 시스템**: 편향된 훈련 데이터 분석, 민감한 정보 처리, AI 시스템의 성능과 한계, 형사사법 시스템에서의 AI 사용, 주택 임대 시 AI 알고리즘 차별 문제 등이 다루어집니다."
     ]
    }
   ],
   "source": [
    "# 추상적인 질문 실행\n",
    "answer = rag_chain.stream(\"전체 문서가 다루는 주요 내용에 대해 정리해주세요.\")\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45f57edb",
   "metadata": {},
   "source": [
    "[LangSmith 링크](https://smith.langchain.com/public/bf58bdc0-ae03-4793-89ed-3d2bc95bd331/r)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "02c6641e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "앤스로픽(Anthropic)에 대한 투자 관련 내용을 요약하면 다음과 같습니다:\n",
      "\n",
      "구글은 AI 스타트업 앤스로픽에 최대 20억 달러를 투자하고 있으며, 이 중 5억 달러는 이미 투자되었습니다. 앤스로픽은 챗GPT의 경쟁 제품인 '클로드(Claude)'라는 대형 언어 모델을 개발 중이며, 구글의 칩을 사용하여 파트너십을 확장할 계획입니다. 이러한 투자는 구글이 생성 AI 분야에서의 협력을 강화하고, 클라우드 컴퓨팅 시장에서 경쟁력을 높이기 위한 전략의 일환입니다."
     ]
    }
   ],
   "source": [
    "# mid level 질문 실행\n",
    "answer = rag_chain.stream(\"Anthropic 에 투자 관련된 내용을 요약하세요.\")\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea6296b2",
   "metadata": {},
   "source": [
    "[LangSmith 링크](https://smith.langchain.com/public/d2869acc-ac9b-4d4d-85b9-33a73b43b535/r)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "6e8193fb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "삼성전자가 개발한 생성형 AI의 이름은 '삼성 가우스'이며, 발표일은 2023년 11월 8일입니다."
     ]
    }
   ],
   "source": [
    "# Low Level 질문 실행\n",
    "answer = rag_chain.stream(\"삼성전자가 개발한 생성형 AI 의 이름과 발표일은?\")\n",
    "stream_response(answer)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
